<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <script>
      window.mlogvis = (function () {

        // add custom date interpolator
        d3.interpolators.push( function(a, b) {
            if ((a instanceof Date) && (b instanceof Date)) {
              var a_ts = a.getTime();
              var b_ts = b.getTime();
              return function(t) {
                return new Date(a_ts * (1-t) + b_ts * t);
              };
            }
        });


        function MLogVis (data, cfg) {
            // define some default values
            defaults = {
                container: "body",
                legend: null,
                group_field: "namespace",
                yaxis_field: "duration",
                view_callback: null, 
                select_callback: null
            }

            // bind defaults to this object
            for (var key in defaults) {
                this[key] = defaults[key];
            }

            // overwrite with config arguments
            for (var key in cfg) {
                this[key] = cfg[key];
            }

            // set up width, height, margins
            this.margin = {top: 20, right: 50, bottom: 50, left: 50};
            this.width = 980 - this.margin.left - this.margin.right,
            this.height = 500 - this.margin.top - this.margin.bottom;

            // multi-scale time axis (only local var)
            var customTimeFormat = this._time_format([
                [d3.time.format.utc("%b %d"), function(d) { return true}],
                [d3.time.format.utc("%H:%M"), function(d) { return d.getUTCHours(); }],
                [d3.time.format.utc("%H:%M:%S"), function(d) { return d.getUTCSeconds(); }],
                [d3.time.format.utc(".%L"), function(d) { return d.getUTCMilliseconds(); }],
                [d3.time.format.utc("%b %d"), function(d, i) { return i%10 == 0; }]
            ]);

            // flag to enable zoom box
            this.zoom_enabled = false;

            // flag to enable logscale
            this.logscale_enabled = false;

            // container for visible / invisible groups
            this.visible_groups = {}

            // timer for view updates
            this._view_timer_id = null;

            // bind data
            this.data = data;

            // create x scale, from [events_min, events_max] to [0, width]
            this.x = d3.time.scale.utc()
                .range([0, this.width]);

            // create y scale, from [0, 100] to [0, height]
            this.y = d3.scale.linear()
                .range([this.height, 0]);

            // calculate min and max dates
            this._recalc_data_extents();

            this.x.domain(this.data_lim_x);
            this.y.domain(this.data_lim_y);

            // var color scale (10 colors)
            this.c = d3.scale.category10();

            this.xAxis = d3.svg.axis()
                .scale(this.x)
                .orient("bottom")
                .ticks(10)
                .tickPadding(20)
                .tickSize(-this.height)
                .tickFormat(customTimeFormat);

            this.yAxis = d3.svg.axis()
                .scale(this.y)
                .orient("left")
                .ticks(5)
                .tickSize(-this.width);

            this.zoom = d3.behavior.zoom()
                .x(this.x)
                .y(this.y)
                .scaleExtent([1, 100000])
                .on("zoom", this._zoomed.bind(this));

            var obj = this;

            // svg container, attach it to "container" 
            this.svg = d3.select(this.container).append("svg")
                .attr("width", this.width + this.margin.left + this.margin.right)
                .attr("height", this.height + this.margin.top + this.margin.bottom)
                .append("g")
                    .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")")
                    .call(this.zoom)
                .append("g")
                    .on("mousedown", function() {
                        if (!obj.zoom_enabled) return;
                        var e = this,
                            origin = d3.mouse(e),
                            rect = obj.svg.append("rect").attr("class", "zoom");
                        d3.select("body").classed("noselect", true);
                        origin[0] = Math.max(0, Math.min(obj.width, origin[0]));
                        origin[1] = Math.max(0, Math.min(obj.height, origin[1]));
                        d3.select(window)
                          .on("mousemove.zoom_rect", function() {
                            var m = d3.mouse(e);
                            m[0] = Math.max(0, Math.min(obj.width, m[0]));
                            m[1] = Math.max(0, Math.min(obj.height, m[1]));
                            rect.attr("x", Math.min(origin[0], m[0]))
                              .attr("y", Math.min(origin[1], m[1]))
                              .attr("width", Math.abs(m[0] - origin[0]))
                              .attr("height", Math.abs(m[1] - origin[1]));
                          })
                      
                        .on("mouseup.zoom_rect", function() {
                          d3.select(window).on("mousemove.zoom_rect", null).on("mouseup.zoom_rect", null);
                          d3.select("body").classed("noselect", false);
                          var m = d3.mouse(e);
                          m[0] = Math.max(0, Math.min(obj.width, m[0]));
                          m[1] = Math.max(0, Math.min(obj.height, m[1]));
                          if (m[0] !== origin[0] && m[1] !== origin[1]) {
                            obj.zoom.x(obj.x.domain([origin[0], m[0]].map(obj.x.invert).sort(d3.ascending)))
                                    .y(obj.y.domain([origin[1], m[1]].map(obj.y.invert).sort(d3.ascending)));
                          }
                          rect.remove();
                          obj._zoomed();
                        }, true);
                        d3.event.stopPropagation();
                      });



            // svg legend
            if (this.legend) {
              d3.select(this.legend)
                .append("svg")
                .attr("width", 250);
            }

            // background and axes
            this.svg.append("rect")
                .attr("width", this.width)
                .attr("height", this.height);

            this.svg.append("g")
                .attr("class", "x axis")
                .attr("transform", "translate(0," + this.height + ")")
                .call(this.xAxis);

            this.svg.append("g")
                .attr("class", "y axis")
                .call(this.yAxis);

            // render plot and legend
            this.render_plot();
            this.render_legend();
        }

        MLogVis.prototype._time_format = function(formats) {
            return function(date, idx) {
                var i = formats.length - 1, f = formats[i];
                while (!f[1](date, idx)) f = formats[--i];
                return f[0](date, idx);
            }
        };

        MLogVis.prototype.render_legend = function() {
          if (this.legend === null) {
            return;
          }

          var legend_svg = d3.select(this.legend).select("svg")
            .attr("height", (this.c.domain().length) * 15);

          var items = legend_svg.selectAll("g")
            .data(this.c.domain());

          var obj = this;

          items.enter()
            .append("g")
              .attr("transform", function (d,i) { return "translate(0, " + (i)*15 + ")" })
              .on("click", function(d) { 
                if (d3.event.altKey) {
                  // count visible groups
                  var num_vis_groups = 0;
                  for (var o in obj.visible_groups) {
                      if (obj.visible_groups[o]) {
                        num_vis_groups ++;
                      }
                  }
                  if (obj._is_visible_group(d) && (num_vis_groups == 1)) {
                    obj.set_group_visibility(obj.c.domain(), true);
                  } else {
                    obj.set_group_visibility(obj.c.domain(), false);
                    obj.set_group_visibility(d, true);
                  }
                } else {
                  obj.set_group_visibility(d, !obj._is_visible_group(d));
                }
                obj.render_legend();
              })
              .each(function () {
                var g = d3.select(this)
                  .style("cursor", "default");

                g.append("circle")
                  .attr("cx", 5)
                  .attr("cy", 5)
                  .attr("r", 4)
                  .style("stroke", function (d) { return obj.c(d); })
                  .style("stroke-width", 2)

                g.append("text")
                  .attr("dy", function(d,i) {return (i)*1.5+0.8 + "em"; })
                  .attr("dx", "15px")
                  .attr("fill", "black")
                  .text(function (d) { return d; });
              });

          items
            .each(function () {
              var g = d3.select(this);
              g.select("text")
                .attr("dy", function(d,i) {return (i)*1.5+0.8 + "em"; })
                .text(function (d) { return d; });
              g.select("circle")
                .style("fill", function (d) { return obj._is_visible_group(d) ? obj.c(d) : "white" });               
            });

          items.exit()
            .each(function () {
              var g = d3.select(this);
              g.select("circle").remove();
              g.select("text").remove();
              g.remove();
            });
        }

        MLogVis.prototype._is_visible = function(d) {
          // only include data point if it has the yaxis_field and a datetime
          if (!d[this.yaxis_field] || !d.datetime) {
            return false;
          }
          // only bind data that is within the viewport and is visible
          var group = this._is_visible_group(d[this.group_field]);
          var xdt = new Date(d['datetime']);
          var xdomain = this.x.domain();
          var ydt = d[this.yaxis_field];
          var ydomain = this.y.domain();
          return (group && (xdt > xdomain[0]) && (xdt < xdomain[1]) && (ydt > ydomain[0]) && (ydt < ydomain[1]));
        };

        MLogVis.prototype.render_plot = function() {
          // filter data to plot in case yaxis_field has changed and bind by _id
          var circles = this.svg.selectAll('circle')
            .data(this.data.filter( this._is_visible.bind(this) ));

          circles
            .enter()
              .append('circle')
              .attr('r', 4)
              .attr("class", "event")
            .on("click", (function (d, i) { 
              if (typeof this.select_callback === "function") {
                this.select_callback(d, i); 
              }
            }).bind(this));

          circles
            .style('fill', (function(d) { return this.c(d[this.group_field]) }).bind(this))
            // .style('display', (function (d) { return this._is_visible_group(d[this.group_field]) ? "block" : "none" }).bind(this))
            .attr('cx', (function(d) { return this.x(new Date(d['datetime'])) }).bind(this))
            .attr('cy', (function(d) { return this.y(d[this.yaxis_field]) }).bind(this));

          circles.exit().remove();
        };


        MLogVis.prototype._is_visible_group = function(group) {
            return !(group in this.visible_groups) || this.visible_groups[group];
        };


        MLogVis.prototype.set_group_visibility = function(groups, visible) {
          // expects a string or array of strings, changes visibility for given group(s) according to visible flag (true/false)
          if (typeof groups === "string") {
            groups = [groups];
          }

          for (var g in groups) {
            var group = groups[g];
            this.visible_groups[group] = visible;
          }
          this.render_plot();
        };


        MLogVis.prototype.enable_zoom = function(flag) {
            this.zoom_enabled = flag;
        };

        MLogVis.prototype.enable_logscale = function(flag, time) {
            if (this.logscale_enabled == flag) {
              return;
            }
            var y_scale = flag ? d3.scale.log() : d3.scale.linear();
            this.logscale_enabled = flag;
            this._recalc_data_extents();

            y_scale
              .domain(this.data_lim_y)
              .range(this.y.range());

            this.y = y_scale;
            this.x.domain(this.data_lim_x);

            // update zoom (update x and y because zoom's translate/scale gets reset)
            zoom_tr = this.zoom.translate()
            zoom_sc = this.zoom.scale()
            this.zoom.y(this.y);
            this.zoom.x(this.x);
            this.zoom.translate(zoom_tr);
            this.zoom.scale(zoom_sc);

            this.yAxis.scale(this.y);
            this.svg.select(".y.axis")
              .transition()
              .duration(time)
              .call(this.yAxis);

            this.svg.selectAll('circle')
              .data(this.data.filter( (function(d) { return d[this.yaxis_field] && d.datetime }).bind(this) ), function(d) { return d['_id'] })
              .transition()
                .duration(time)
                .attr('cy', (function(d) { return this.y(d[this.yaxis_field]) }).bind(this));
        };


        MLogVis.prototype.set_yaxis_field = function(field) {
            this.yaxis_field = field;
            this._recalc_data_extents();
            this.reset(0);
            this.render_plot();
            this.render_legend();
        };

        MLogVis.prototype.group_by = function(field) {
            // reset categories
            this.c = d3.scale.category10();
            this.group_field = field;
            // reset visible groups, all groups are visible by default
            this.visible_groups = {};
            this.render_plot();
            this.render_legend();
        };

        MLogVis.prototype.get_groups = function() {
          return this.c.domain();
        }  

        MLogVis.prototype._view_callback = function() {
          // triggers a view_callback call after 100ms of not changing the view anymore. 
          // this behavior prevents constant callback triggers during continuous tweens, panning and mousewheel zoom
          if (typeof this.view_callback === "function") {
            window.clearTimeout(this._view_timer_id);
            this._view_timer_id = window.setTimeout((function () { 
              // calculate new view
              var view_bounds = {};
              view_bounds["datetime"] = this.x.domain();
              view_bounds[this.yaxis_field] = this.y.domain();
              this.view_callback( view_bounds ); 
            }).bind(this), 100);
          }
        }

        MLogVis.prototype._zoomed = function() {
          // queries the zoom object and updates the view accordingly
          var zoom_tr = this.zoom.translate();
          this.zoom.translate( [zoom_tr[0], Math.min(0, zoom_tr[1])] );
          this.svg.select(".x.axis").call(this.xAxis);
          this.svg.select(".y.axis").call(this.yAxis);
          
          this._view_callback();
          this.render_plot();
        };

        MLogVis.prototype._recalc_data_extents = function() {
          var data = this.data.filter( (function(d) { return d[this.yaxis_field] && d.datetime }).bind(this) );

          this.data_lim_x = d3.extent(data, function(d, i) { return new Date(d.datetime) });
          this.data_lim_y = d3.extent(data, (function(d, i) { return d[this.yaxis_field] }).bind(this));
          // bit of buffer on the top and bottom

          if (!this.logscale_enabled) {
            this.data_lim_y[0] = 0;
            this.data_lim_y[1] *= 1.1;
          } else {       
            this.data_lim_y[1] *= 1.5;
          }          
        }            

        MLogVis.prototype.highlight = function(condition) {
          var circles = this.svg.selectAll('circle')
            .data(this.data, function(d) { return d['_id'] });

          circles
            .attr("r", 4)
            .style("stroke", null);

          circles
            .filter(condition)
            .attr("r", 5)
            .style('stroke', 'yellow')
            .style('stroke-width', 3)
            .style('stroke-opacity', 1)
        };

        MLogVis.prototype.reset = function(time) {
          // transition to original view frame but keep group selection
          if (time === undefined) {
            time = 500;
          }

          this._recalc_data_extents();

          var bind_f = function(obj) {
            // wrapper to bind this to obj
            return function() {
              // closure
              var ix = d3.interpolate(obj.x.domain(), obj.data_lim_x);
              var iy = d3.interpolate(obj.y.domain(), obj.data_lim_y);
              return function(t) {
                obj.zoom
                  .x(obj.x.domain(ix(t)))
                  .y(obj.y.domain(iy(t)));
                obj._zoomed();
              };
            };
          };

          d3.transition().duration(time).tween("zoom", bind_f(this));
        };

        // filter, calc, scales
        // zoom checkbox, logscale checkbox, color grouping 

        // init function
        var mlogvis = {
            init: function (data, cfg) {
                return new MLogVis(data, cfg);
            }
        };

        return mlogvis;

      }());



    </script>

    <style>
      body {
        position: relative;
        width: 960px;
        font-size: 11px;
        font-family: verdana;
      }

      svg {
        font: 10px sans-serif;
      }

      rect {
        fill: #fff;
      }

      circle.event {
        fill: steelblue;
        opacity: 0.5;
      }

      .axis path,
      .axis line {
        fill: none;
        stroke: #ddd;
      }

      .axis text {
        -webkit-user-select: none; /* Chrome/Safari */        
        -moz-user-select: none; /* Firefox */
        -ms-user-select: none; /* IE10+ */

        /* Rules below not implemented in browsers yet */
        -o-user-select: none;
        user-select: none;
      }

      rect.zoom {
        stroke: steelblue;
        fill: lightsteelblue;
        fill-opacity: 0.2;
      }

      div#controls {
        position: absolute;
        width: 300px;
        right: -300px;
        top: 30px;
      }

      #controls .fieldset {
        border-color: #eee;
        border-style: solid;
      }

      #legend {
        padding-left: 4px;
      }

      #logline {
        width: 910px;
        min-height: 40px;
        border: 1px solid #ddd;
        overflow: scroll;
        margin-left: 10px;
        padding: 5px;
        font-family: monospace;
        font-size: 11px;
      }

    </style>
  </head>

  <body>
    <div id="plot"></div>

    <div id="controls">
      <label for="zoom-check"><input type="checkbox" id="zoom-check"><u>z</u>oom</label>
      <button id="reset">reset</button><br>
      <label for="log-check"><input type="checkbox" id="log-check"><u>l</u>og scale</label>
      <p>
      <fieldset class="fieldset" id="y-axis">
        <legend>y-axis</legend>
        <label for="duration"><input id="duration" type="radio" name="y-axis" checked>duration</label><br>
        <label for="nscanned"><input id="nscanned" type="radio" name="y-axis">nscanned</label><br>
        <label for="numYields"><input id="numYields" type="radio" name="y-axis">num yields</label><br>
        <label for="w"><input id="w" type="radio" name="y-axis">write lock</label><br>
        <label for="r"><input id="r" type="radio" name="y-axis">read lock</label>
      </fieldset><br>
      <p>
      <fieldset class="fieldset" id="group-by">
        <legend>group by</legend>
        <label for="namespace"><input id="namespace" type="radio" name="group" checked>namespace</label><br>
        <label for="operation"><input id="operation" type="radio" name="group">operation</label><br>
        <label for="thread"><input id="thread" type="radio" name="group">thread</label><br><br>
        <div id="legend"></div>
      </fieldset><br>
    </div>

    <div id="logline"></div>

    <script>
      // data goes here
      var data = ##REPLACE##;

      // config for visualizer
      var cfg = {
        container: "#plot",
        legend: "#legend",
        yaxis_field: "duration",                // pick any numeric field in the document, e.g. "nscanned"
        group_field: "namespace",               // pick any categorical field in the document, e.g. "thread", "operation", "log2code"
        // view_callback: function (v) {           // callback for changes in view bounding box
        //   // example: print message to console
        //   console.log("view changed, get data from server!", v);
        // },                    
        select_callback: function (d, i) {      // callback for click on element, or null
          // example: print log line string to console
          if (d.line_str) {
            d3.select("#logline").html(d.line_str);
          } else {
            d3.select("#logline").html("line string unavailable. line no: " + d['_id'] + " document:" + JSON.stringify(d))
          }
        },   
      };    

      // create visualizer instance
      var visualizer = mlogvis.init(data.data, cfg)

      // connect zoom checkbox to visualizer zoom method
      d3.select("#zoom-check").on("change", function() {
        visualizer.enable_zoom(this.checked);
      });

      // connect logscale checkbox to visualizer logscale method
      d3.select("#log-check").on("change", function() {
        visualizer.enable_logscale(this.checked, 500);
      });

      // connect y-axis radio buttons to visualizer group method
      d3.selectAll("#y-axis input[type=radio]").on("change", function() {
        if (this.checked) {
          visualizer.set_yaxis_field(d3.select(this).property("id"));
        }
      });

      // connect group radio buttons to visualizer group method
      d3.selectAll("#group-by input[type=radio]").on("change", function() {
        if (this.checked) {
          visualizer.group_by(d3.select(this).property("id"));
        }
      });

      // connect reset button to visualizer reset method
      d3.select("button#reset").on("click", function () { visualizer.reset(500); });

    </script>
  </body>
</html>
